package com.hystericalporpoises.thingamajava;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Stack;

import com.hystericalporpoises.thingamajava.RelationshipMap.RelationshipKey;
import com.hystericalporpoises.thingamajava.annotations.Populatable;
import com.hystericalporpoises.thingamajava.annotations.Populates;
import com.hystericalporpoises.thingamajava.repository.RepositoryAggregator;
import com.hystericalporpoises.thingamajava.repository.RepositoryAlreadyExistsException;
import com.hystericalporpoises.thingamajava.repository.TestObjectRepository;


/**
 * @author ian.g.simpson
 *
 */
public final class Thingamajava {

  private DatabaseProvider databaseProvider;

  /**
   * A map of the class type of a pojo to the repository type
   */
  private Map<Class<?>, TestObjectRepository<?>> repositories =
    new HashMap<Class<?>, TestObjectRepository<?>>();

  /**
   * A linked set that maintains the order in which instances are created so that tear down can
   * occur in the reverse order
   */
  private Set<Class<?>> tearDownClasses = new LinkedHashSet<Class<?>>();

  private Map<String, ManagedInstance> instanceMap = new HashMap<String, ManagedInstance>();

  private RelationshipMap relationshipMap = new RelationshipMap();

  private Object instanceUnderTest;

  public static Thingamajava createForTest(DatabaseProvider databaseProvider,
    RepositoryAggregator aggregator, Object test) {

    Thingamajava thingamajava = new Thingamajava();

    thingamajava.databaseProvider = databaseProvider;
    thingamajava.addRepositoriesFromAggregator(aggregator);

    thingamajava.analyzeTest(test);

    return thingamajava;
  }

  public static Thingamajava createForTest(ThingamajavaDescriptor test, Object unitTest) {

    return createForTest(test.getDatabaseProvider(), test.getRepositoryAggregator(), unitTest);
  }

  private Thingamajava() {}

  private void analyzeTest(Object instanceUnderTest) {
    Class<?> classUnderTest = instanceUnderTest.getClass();

    try {

      // Find all the fields of the class
      Field[] fields = classUnderTest.getDeclaredFields();

      for (Field field : fields) {

        // If it's populatable, send it off to be created
        if (field.isAnnotationPresent(Populatable.class) || field.isAnnotationPresent(Populates.class)) {
          createPopulatableInstance(instanceUnderTest, field);
        }
        if (field.isAnnotationPresent(Populates.class)) {
          markPopulationProperties(instanceUnderTest, field);
        }
      }

    } catch (Exception e) {
      e.printStackTrace();
    }

    figureOutWhatToDoWithEverything();

  }

  private void figureOutWhatToDoWithEverything() {
    for (String key : instanceMap.keySet()) {
      ManagedInstance instance = instanceMap.get(key);
      if (!instance.isManaged()) {
        figureOutWhatToDoWith(key, instance);
      }
    }
  }

  private void figureOutWhatToDoWith(String name, ManagedInstance instance) {

    // Get the actual car instance, which is populated earlier
    Object instanceToManage = instance.getInstance();

    // Comments are for my own sanity...
    // name = car
    // instance = subaru
    // for every relationship key starting with car.
    for (RelationshipKey relationshipKey : relationshipMap.getKeysStartingWith(name)) {
      // found car.engine
      // test field name is diesel
      String testFieldName = relationshipMap.get(relationshipKey.getMapKey());
      // find the 'diesel' field that was discovered earlier
      ManagedInstance dependency = instanceMap.get(testFieldName);
      // if it's not managed, we should figure out what needs to be resolved on it
      if (!dependency.isManaged()) {
        figureOutWhatToDoWith(testFieldName, dependency);
      }

      try {
        // find the 'engine' field of the 'car' instance's class
        Field property = instanceToManage.getClass().getDeclaredField(relationshipKey.getFieldKey());
        // make sure engine is accessible
        property.setAccessible(true);
        //
        property.set(instanceToManage, dependency.getInstance());
        // The three methods below should no longer be necessary
//        Field mappedField = instanceUnderTest.getClass().getDeclaredField(testFieldName);
//        mappedField.setAccessible(true);
//        property.set(instance, mappedField.get(instanceUnderTest));
      }
      catch (Exception e) {
        e.printStackTrace();
      }
    }

    Class<?> instanceClass = instanceToManage.getClass();

    @SuppressWarnings("unchecked")
    // Necessary because '?' != Object
    TestObjectRepository<Object> repository = (TestObjectRepository<Object>) repositories.get(instanceClass);
    repository.manageInstance(instanceToManage);
  }

  private void markPopulationProperties(Object instanceUnderTest, Field field) throws IllegalAccessException {
    Populates populates = field.getAnnotation(Populates.class);
    for (String populate : populates.value()) {
      relationshipMap.put(populate, field.getName());
    }
  }

  private void createPopulatableInstance(Object instanceUnderTest, Field field)
    throws IllegalAccessException, InstantiationException {

    // Find the type of the field
    Class<?> type = field.getType();

    // Make it accessible since it's probably private
    field.setAccessible(true);

    // Find the repository associated with that type
    // FIXME: add NPE checking
    TestObjectRepository<?> repository = repositories.get(type);

    // Create a new instance based on the repo's logic
    Object newInstance = repository.getInstance();

    // Set the instance in the instance under test
    field.set(instanceUnderTest, newInstance);

    ManagedInstance instance = new ManagedInstance();
    instance.setInstance(newInstance);

    // Store the field to reference later
    instanceMap.put(field.getName(), instance);

  }

  private void addRepositoriesFromAggregator(RepositoryAggregator aggregator) {
    for (TestObjectRepository<?> repository : aggregator.getRepositories()) {
      addRepository(repository);
    }
  }

  public final <T> void addRepository(TestObjectRepository<T> repository) {

    // passing a null repository is going to cause an NPE, may as well give the user a reason
    if (repository == null) {
      throw new NullPointerException("You cannot assign a null repository to a factory");
    }

    // Create an instance of the type that repository stores so that we can derive the class
    T instance = repository.getInstance();

    // Tell the repository about the database provider
    repository.setDatabaseProvider(databaseProvider);

    Class<?> instanceType = instance.getClass();

    // Check to see if we already have a repository associated with this type
    if (repositories.containsKey(instanceType)) {

      TestObjectRepository<?> existingRepo = repositories.get(instanceType);

      // If the existing is not null (should never be null) and isn't equal to the new one, fail
      if (existingRepo != null && !existingRepo.equals(repository)) {
        throw new RepositoryAlreadyExistsException("Repository already exists for type "
          + instanceType + ": " + existingRepo.getClass().getCanonicalName()
          + " (duplicate repository was " + repository.getClass().getCanonicalName() + ")");
      }

      // If the existing is equal, we don't need to do anything else
      if (existingRepo.equals(repository)) {
        return;
      }

    }

    // At this point, it must be a new repository, so add it to the map
    repositories.put(instanceType, repository);
  }

  public final <T> T getManagedInstanceOf(Class<T> clazz) {

    // Find the repository for the class type requested.  If it doesn't exist, fail
    TestObjectRepository<T> repository = getRepositoryFor(clazz);
    if (repository == null) {
      throw new RuntimeException("Repository requested for " + clazz.getCanonicalName() +
        " does not exist in this factory");
    }

    // Add the class asked for to the tear down history so that we can remember when to tear down
    tearDownClasses.add(clazz);
    T instance = repository.getInstance();
    repository.manageInstance(instance);
    // Return whatever managed instance the repository gives us
    return instance;
  }

  public final <T> T manage(T instance) {
    String instanceKey = "";
    for(Entry<String, ManagedInstance> entry : instanceMap.entrySet()) {
      if (entry.getValue().getInstance() == instance) {
        instanceKey = entry.getKey();
      }
    }
    for (String key : relationshipMap.keySet()) {
      if (key.startsWith(instanceKey) && !key.equals(instanceKey)) {
        String propertyKey = key.substring(instanceKey.length() + 1);
        try {
          Field property = instance.getClass().getDeclaredField(propertyKey);
          property.setAccessible(true);
          String fieldName = relationshipMap.get(key);
          Field mappedField = instanceUnderTest.getClass().getDeclaredField(fieldName);
          mappedField.setAccessible(true);
          property.set(instance, mappedField.get(instanceUnderTest));
        }
        catch (Exception e) {
          e.printStackTrace();
        }
      }
    }
    return instance;
  }

  private final <T> TestObjectRepository<T> getRepositoryFor(Class<T> clazz) {
    @SuppressWarnings("unchecked")
    TestObjectRepository<T> repository = (TestObjectRepository<T>) repositories.get(clazz);
    return repository;
  }

  public final void tearDown() {
    Stack<Class<?>> tearDownStack = new Stack<Class<?>>();
    for (Class<?> tearDownClass : tearDownClasses) {
      tearDownStack.push(tearDownClass);
    }
    for (Class<?> tearDownClass : tearDownStack) {
      repositories.get(tearDownClass).tearDown();
    }
  }

}
